package network;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.security.AlgorithmParameterGenerator;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.KeyAgreement;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.modes.PaddedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 *
 * @author lupomobile
 */
public class Client implements Runnable {

    private int port;
    private boolean stop;
    private Socket socket;
    private PrivateKey RSAClientPrivateKey;
    private PublicKey RSAClientPublicKey, RSAServerPublicKey;
    private int status; //0: pronto per autenticarsi
    private final int KEYSIZE = 256;
    private PrivateKey Sc;
    private PublicKey gSc;
    private PublicKey gSs;
    private byte[] session_key;

    public Client(int p) {
        port = p;
        status = 0;
        BufferedReader keyIn;
        BigInteger keyModulus;
        BigInteger keyExponent;
        KeyFactory fact;
        Security.addProvider(new BouncyCastleProvider());
        try {
            //Rileggo la chiave 
            keyIn = new BufferedReader(new FileReader("cPrivate.key"));
            keyModulus = new BigInteger(keyIn.readLine(), 16);
            keyExponent = new BigInteger(keyIn.readLine(), 16);
            keyIn.close();
            RSAPrivateKeySpec keyspec1 = new RSAPrivateKeySpec(keyModulus, keyExponent);
            fact = KeyFactory.getInstance("RSA");
            RSAClientPrivateKey = fact.generatePrivate(keyspec1);

            keyIn = new BufferedReader(new FileReader("cPublic.key"));
            keyModulus = new BigInteger(keyIn.readLine(), 16);
            keyExponent = new BigInteger(keyIn.readLine(), 16);
            keyIn.close();
            RSAPublicKeySpec keyspec2 = new RSAPublicKeySpec(keyModulus, keyExponent);
            fact = KeyFactory.getInstance("RSA");
            RSAClientPublicKey = fact.generatePublic(keyspec2);

            keyIn = new BufferedReader(new FileReader("sPublic.key"));
            keyModulus = new BigInteger(keyIn.readLine(), 16);
            keyExponent = new BigInteger(keyIn.readLine(), 16);
            keyIn.close();
            RSAPublicKeySpec keyspec3 = new RSAPublicKeySpec(keyModulus, keyExponent);
            fact = KeyFactory.getInstance("RSA");
            RSAServerPublicKey = fact.generatePublic(keyspec3);

        } catch (FileNotFoundException e) {
           Network.writeConsole(0,"File non trovato. Programma terminato");
            System.exit(0);
        } catch (NumberFormatException e) {
           Network.writeConsole(0,"Chiave non letta in modo corretto. Programma terminato");
            System.exit(0);
        } catch (NoSuchAlgorithmException e) {
           Network.writeConsole(0,"Algoritmo non supportato. Programma terminato");
        } catch (InvalidKeySpecException e) {
           Network.writeConsole(0,"Spefiche RSA errate. Programma terminato");
            System.exit(0);
        } catch (IOException e) {
           Network.writeConsole(0,"Inizializzazione socket fallita. Programma Terminato");
            System.exit(0);
        }
    }

    private void init() {
        try {
            socket = new Socket("127.0.0.1", port);
        } catch (UnknownHostException ex) {
           Network.writeConsole(0,"Host non raggingibile");
            System.exit(0);
        } catch (IOException ex) {
           Network.writeConsole(0,"Errore sconosciuto nel socket");
            System.exit(0);
        }
    }

    @Override
    public void run() {
        init();
        try {
            auth_start();
            try {
                socket.getOutputStream().write(new TLV(TLV.SESSION_END, new byte[1]).generatePackage()); // Termino la connessione
                socket.close();
            } catch (IOException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }
        } catch (AbortConnection ex) {
            try {
                socket.close();
                stop();
            } catch (IOException ex1) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex1);
            }
        } catch (InvalidParameterSpecException ex) {
        } catch (NoSuchAlgorithmException ex) {
        }
    }

    private void stop() {
        stop = false;
    }

    //Il client genera i parameti p e g di DH e li invia al server
    private void auth_start() throws InvalidParameterSpecException, NoSuchAlgorithmException, AbortConnection {
        BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
        String responce = new String();
        
       Network.writeConsole(2,"CLIENT DICE: inizio autenticazione");
        try {
            AlgorithmParameterGenerator paramGen = null;

            paramGen = AlgorithmParameterGenerator.getInstance("DH", "BC");
            paramGen.init(KEYSIZE);

            AlgorithmParameters param = paramGen.generateParameters();
            DHParameterSpec dhparam = (DHParameterSpec) param.getParameterSpec(DHParameterSpec.class);

            //Invio parametri DH
            BigInteger p = dhparam.getP();
            BigInteger g = dhparam.getG();

            TLV pp = new TLV(TLV.AUTH_INIT_SEND_P, p.toByteArray());
            TLV gp = new TLV(TLV.AUTH_INIT_SEND_G, g.toByteArray());
            try {
                socket.getOutputStream().write(pp.generatePackage());
                socket.getOutputStream().write(gp.generatePackage());
                
               Network.writeConsole(2,"CLIENT DICE: Parametri Diffie Hellman inviati al server");
            } catch (IOException ex) {
            }
            //Fine invio parametri

            //Generazione del numero casuale Sc e della sua esponenziazione g^Sc
            KeyPairGenerator generator = null;
            try {
                generator = KeyPairGenerator.getInstance("DH");
                generator.initialize(dhparam);
            } catch (NoSuchAlgorithmException ex) {
            } catch (InvalidAlgorithmParameterException ex) {
            }
            KeyPair keys = generator.genKeyPair();
            Sc = keys.getPrivate();
            gSc = keys.getPublic();

            //Invio di g^Sc al server
            byte[] data = ((DHPublicKey) gSc).getY().toByteArray();
            try {
                
               Network.writeConsole(0,"Vuoi inviare l'esponenziazione del client corretta?(s/n)");
                responce = buff.readLine();
                if (responce.equals("s")) {
                    socket.getOutputStream().write(new TLV(TLV.AUTH_START, data).generatePackage());
                } else {
                    data[0] = 11;
                    socket.getOutputStream().write(new TLV(TLV.AUTH_START, data).generatePackage());
                }
                
               Network.writeConsole(2,"CLIENT DICE: esponenziazione inviata al server (gSc)");
            } catch (IOException ex) {
            }

            //Invio completato. Resto in attesa di risposta
            
           Network.writeConsole(2,"CLIENT DICE: in attesa dell'esponenziazione del server");
            TLV pack = TLV.fetchPackage(socket);
            if (pack == null || pack.getType() == 0) {
                
               Network.writeConsole(2,"CLIENT DICE: il server ha chiuso la connessione con errore. Programma terminato");
                throw new AbortConnection();
            }
            if (pack.getType() == TLV.AUTH_RESPONCE) {
                try {
                    
                   Network.writeConsole(2,"CLIENT DICE: esponenziazione del server ricevuta (gSs)");
                    BigInteger y = new BigInteger(pack.getValue());
                    KeyFactory kf = KeyFactory.getInstance("DH");
                    DHPublicKeySpec a = new DHPublicKeySpec(y, p, g);
                    gSs = kf.generatePublic(a);
                } catch (InvalidKeySpecException ex) {
                    Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                
               Network.writeConsole(2,"CLIENT DICE: ricevuto pacchetto inaspettato, era prevista una esponenziazione. Connessione Terminata");
                socket.close();
                throw new AbortConnection();
            }

            //Posso derivare la chiave di sessione
            KeyAgreement ka = KeyAgreement.getInstance("DH");
            ka.init(Sc);
            ka.doPhase(gSs, true);
            session_key = ka.generateSecret();
            
           Network.writeConsole(2,"CLIENT DICE: chiave di sessione calcolata");
            
           Network.writeConsole(2,"ckey:" + new BigInteger(session_key).toString(16));


            //Verifico l'identità del server
           Network.writeConsole(2,"CLIENT DICE: in attesa in attesa di verificare l'identità del server");
            pack = TLV.fetchPackage(socket); //prelevo il pacchetto
            if (pack == null || pack.getType() == 0) {
               Network.writeConsole(2,"CLIENT DICE: il server ha chiuso la connessione con errore. Programma terminato");
                throw new AbortConnection();
            }
            if (pack.getType() == TLV.AUTH_RESPONCE_SIGN) {
               Network.writeConsole(2,"CLIENT DICE: prova di identità del server ricevuta");
                //Decripto il contenuto
                BlockCipher engine = new AESEngine();
                BufferedBlockCipher cipher = new PaddedBlockCipher(new CBCBlockCipher(engine));

                cipher.init(false, new KeyParameter(session_key)); //True sta per cifratura, false per decifratura

                byte[] pre_decrypted = new byte[cipher.getOutputSize(pack.getValue().length)];

                int outputLen = cipher.processBytes(pack.getValue(), 0, pack.getValue().length, pre_decrypted, 0);
                cipher.doFinal(pre_decrypted, outputLen);
                
               Network.writeConsole(2,"CLIENT DICE: prova di identità del server decifrata");
                //Rimuovo lo zero padding
                byte[] decrypted = new byte[outputLen];
                ByteBuffer.wrap(pre_decrypted).get(decrypted, 0, outputLen);

                //Ricostruisco il messaggio che dovrebbe essere firmato
                BigInteger gSs_number = ((DHPublicKey) gSs).getY();
                BigInteger gSc_number = ((DHPublicKey) gSc).getY();
                ByteBuffer proof = ByteBuffer.wrap(new byte[gSc_number.toByteArray().length + gSs_number.toByteArray().length]);
                proof.put(gSc_number.toByteArray());
                proof.put(gSs_number.toByteArray());

                //Firmo con RSA il pacchetto
                Signature sign = Signature.getInstance("SHA1withRSA");
                sign.initVerify(RSAServerPublicKey);
                sign.update(proof.array());
                boolean verify = sign.verify(decrypted);

                //Se la firma è verificata posso procedere con la mia autenticazione
                if (verify) {
                    
                   Network.writeConsole(4,"CLIENT DICE: firma verificata! Client autenticato!!!!!!");
                    //Costruisco la challenge
                    proof = ByteBuffer.wrap(new byte[gSs_number.toByteArray().length + gSc_number.toByteArray().length]);
                    proof.put(gSc_number.toByteArray());
                    proof.put(gSs_number.toByteArray());

                    //Firmo la challenge
                    sign = Signature.getInstance("SHA1withRSA");
                    sign.initSign(RSAClientPrivateKey);
                    sign.update(proof.array());
                    byte[] firma = sign.sign();

                    //Cifro la firma
                    engine = new AESEngine();
                    cipher = new PaddedBlockCipher(new CBCBlockCipher(engine));
                    cipher.init(true, new KeyParameter(session_key)); //True sta per cifratura, false per decifratura
                    byte[] encrypted = new byte[cipher.getOutputSize(firma.length)];
                    outputLen = cipher.processBytes(firma, 0, firma.length, encrypted, 0);
                    cipher.doFinal(encrypted, outputLen);

                    //Preparo il pacchetto e invio al server
                    pack = new TLV(TLV.AUTH_RESPONCE_SIGN, encrypted);
                    socket.getOutputStream().write(pack.generatePackage());
                    
                   Network.writeConsole(2,"CLIENT DICE: prova di identità inviata al server");

                    //AUTENTICAZIONE DEL SERVER CONCLUSA
                    p = null;
                    g = null;
                    gSs = null;
                    gSc = null;
                } else {
                   Network.writeConsole(3,"CLIENT_ERRORE: firma del server non valida. Connessione terminata");
                    socket.close();
                    throw new AbortConnection();
                }
            } else {
                throw new AbortConnection();
            }
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        } catch (DataLengthException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalStateException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InvalidCipherTextException ex) {
            
           Network.writeConsole(3,"CLIENT_ERRORE: Cipher Text non valido. Forse la chiave di sessione è errata?");
            throw new AbortConnection();
        } catch (SignatureException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchProviderException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InvalidKeyException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
